﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;

namespace CommLib.Web.VerificationCode
{
    public class GeetestLib
    {
        /// <summary>
        /// SDK版本号
        /// </summary>
        public const string Version = "3.2.0";
        /// <summary>
        /// SDK开发语言
        /// </summary>
        public const string SdkLang = "csharp";
        /// <summary>
        /// 极验验证API URL
        /// </summary>
        protected const string ApiUrl = "http://api.geetest.com";
        /// <summary>
        /// register url
        /// </summary>
        protected const string RegisterUrl = "/register.php";
        /// <summary>
        /// validate url
        /// </summary>
        protected const string ValidateUrl = "/validate.php";
        /// <summary>
        /// 极验验证API服务状态Session Key
        /// </summary>
        public const string GtServerStatusSessionKey = "gt_server_status";
        /// <summary>
        /// 极验验证二次验证表单数据 Chllenge
        /// </summary>
        public const string FnGeetestChallenge = "geetest_challenge";
        /// <summary>
        /// 极验验证二次验证表单数据 Validate
        /// </summary>
        public const string FnGeetestValidate = "geetest_validate";
        /// <summary>
        /// 极验验证二次验证表单数据 Seccode
        /// </summary>
        public const string FnGeetestSeccode = "geetest_seccode";

        private string _userId;
        private string _responseStr;
        private readonly string _captchaId;
        private readonly string _privateKey;

        /// <summary>
        /// 验证成功结果字符串
        /// </summary>
        public const int SuccessResult = 1;
        /// <summary>
        /// 证结失败验果字符串
        /// </summary>
        public const int FailResult = 0;
        /// <summary>
        /// 判定为机器人结果字符串
        /// </summary>
        public const string ForbiddenResult = "forbidden";

        /// <summary>
        /// GeetestLib构造函数
        /// </summary>
        /// <param name="publicKey">极验验证公钥</param>
        /// <param name="privateKey">极验验证私钥</param>
        public GeetestLib(string publicKey, string privateKey)
        {
            _privateKey = privateKey;
            _captchaId = publicKey;
        }
        private static int GetRandomNum()
        {
            var rand = new Random();
            var randRes = rand.Next(100);
            return randRes;
        }
        public async Task<byte> PreProcess(string userId = null)
        {
            if (this._captchaId == null)
            {
                Console.WriteLine("publicKey is null!");
            }
            else
            {
                if(!string.IsNullOrEmpty(userId))
                    _userId = userId;
                var challenge = await RegisterChallenge();
                switch (challenge.Length)
                {
                    case 32:
                        GetSuccessPreProcessRes(challenge);
                        return 1;
                    default:
                        GetFailPreProcessRes();
                        Console.WriteLine("Server regist challenge failed!");
                        break;
                }
            }

            return 0;

        }

        public string GetResponseStr() => this._responseStr;
        /// <summary>
        /// 预处理失败后的返回格式串
        /// </summary>
        private void GetFailPreProcessRes()
        {
            var rand1 = GetRandomNum();
            var rand2 = GetRandomNum();
            var md5Str1 = Security.Md5Encrypt.GetMd5Hash(rand1 + "");
            var md5Str2 = Security.Md5Encrypt.GetMd5Hash(rand2 + "");
            var challenge = md5Str1 + md5Str2.Substring(0, 2);
            _responseStr = "{" + $"\"success\":{0},\"gt\":\"{_captchaId}\",\"challenge\":\"{challenge}\"" + "}";
        }

        /// <summary>
        /// 预处理成功后的标准串
        /// </summary>
        private void GetSuccessPreProcessRes(string challenge)
        {
            challenge = Security.Md5Encrypt.GetMd5Hash(challenge + _privateKey);
            _responseStr = "{" + $"\"success\":{1},\"gt\":\"{_captchaId}\",\"challenge\":\"{challenge}\"" + "}";
        }

        /// <summary>
        /// failback模式的验证方式
        /// </summary>
        /// <param name="challenge">failback模式下用于与validate一起解码答案， 判断验证是否正确</param>
        /// <param name="validate">failback模式下用于与challenge一起解码答案， 判断验证是否正确</param>
        /// <param name="seccode">failback模式下，其实是个没用的参数</param>
        /// <returns>验证结果</returns>
        public int FailbackValidateRequest(string challenge, string validate, string seccode)
        {
            if (!RequestIsLegal(challenge, validate, seccode)) return FailResult;
            var validateStr = validate.Split('_');
            var encodeAns = validateStr[0];
            var encodeFullBgImgIndex = validateStr[1];
            var encodeImgGrpIndex = validateStr[2];
            var decodeAns = DecodeResponse(challenge, encodeAns);
            var decodeFullBgImgIndex = DecodeResponse(challenge, encodeFullBgImgIndex);
            var decodeImgGrpIndex = DecodeResponse(challenge, encodeImgGrpIndex);
            var validateResult = ValidateFailImage(decodeAns, decodeFullBgImgIndex, decodeImgGrpIndex);
            return validateResult;
        }
        private static int ValidateFailImage(int ans, int fullBgIndex, int imgGrpIndex)
        {
            const int thread = 3;
            var fullBgName = Security.Md5Encrypt.GetMd5Hash(fullBgIndex + "").Substring(0, 10);
            var bgName = Security.Md5Encrypt.GetMd5Hash(imgGrpIndex + "").Substring(10, 10);
            var answerDecode = "";
            for (var i = 0; i < 9; i++)
            {
                switch (i % 2)
                {
                    case 0:
                        answerDecode += fullBgName.ElementAt(i);
                        break;
                    case 1:
                        answerDecode += bgName.ElementAt(i);
                        break;
                }
            }
            var xDecode = answerDecode.Substring(4);
            var xInt = Convert.ToInt32(xDecode, 16);
            var result = xInt % 200;
            if (result < 40) result = 40;
            return Math.Abs(ans - result) < thread ? SuccessResult : FailResult;
        }
        private static bool RequestIsLegal(string challenge, string validate, string seccode)
        {
            return !string.IsNullOrEmpty(challenge) && !string.IsNullOrEmpty(validate) && !string.IsNullOrEmpty(seccode);
        }

        /// <summary>
        /// 向gt-server进行二次验证
        /// </summary>
        /// <param name="challenge">本次验证会话的唯一标识</param>
        /// <param name="validate">拖动完成后server端返回的验证结果标识字符串</param>
        /// <param name="seccode">验证结果的校验码，如果gt-server返回的不与这个值相等则表明验证失败</param>
        /// <param name="userId"></param>
        /// <returns>二次验证结果</returns>
        public async Task<int> EnhencedValidateRequest(string challenge, string validate, string seccode, string userId=null)
        {
            if (!RequestIsLegal(challenge, validate, seccode)) return FailResult;
            if (validate.Length <= 0 || !CheckResultByPrivate(challenge, validate)) return FailResult;
            var postdata = new List<KeyValuePair<string, string>>
            {
                new KeyValuePair<string, string>("seccode", seccode),
                new KeyValuePair<string, string>("sdk", "csharp_" + Version)
            };
            if (!string.IsNullOrEmpty(userId))
                postdata.Add(new KeyValuePair<string, string>("user_id", userId));
            var response = "";
            try
            {
                response = await PostValidate(postdata);
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }
            return response.Equals(Security.Md5Encrypt.GetMd5Hash(seccode)) ? SuccessResult : FailResult;
        }

        private async Task<string> RegisterChallenge()
        {
            var url = string.IsNullOrEmpty(_userId) ? $"{ApiUrl}{RegisterUrl}?gt={_captchaId}" : $"{ApiUrl}{RegisterUrl}?gt={_captchaId}&user_id={_userId}";
            var retString = await ReadContentFromGet(url);
            return retString;
        }
        private bool CheckResultByPrivate(string origin, string validate)
        {
            var encodeStr = Security.Md5Encrypt.GetMd5Hash(_privateKey + "geetest" + origin);
            return validate.Equals(encodeStr);
        }

        private static async Task<string> PostValidate(IEnumerable<KeyValuePair<string, string>> data)
        {
            string url = $"{ApiUrl}{ValidateUrl}";
            try
            {
                using (var client = new HttpClient())
                {
                    var postdata = new FormUrlEncodedContent(data);
                    var result = await client.PostAsync(url, postdata);
                    result.EnsureSuccessStatusCode();
                    if (result.StatusCode == System.Net.HttpStatusCode.OK)
                        return await result.Content.ReadAsStringAsync();
                    return null;
                }
            }
            catch
            {
                return null;
            }

        }

        private static async Task<string> ReadContentFromGet(string url)
        {
            try
            {
                using (var client = new HttpClient())
                {
                    client.Timeout = TimeSpan.FromSeconds(2);
                    var result = await client.GetAsync(url);
                    result.EnsureSuccessStatusCode();
                    if (result.StatusCode == System.Net.HttpStatusCode.OK)
                        return await result.Content.ReadAsStringAsync();
                    return null;
                }
            }
            catch
            {
                return "";
            }

        }

        private static int DecodeRandBase(string challenge)
        {
            var baseStr = challenge.Substring(32, 2);
            var tempList = baseStr.Select(t => (int)t).Select(tempAscii => (tempAscii > 57) ? (tempAscii - 87) : (tempAscii - 48)).ToList();
            var result = tempList.ElementAt(0) * 36 + tempList.ElementAt(1);
            return result;
        }
        private static int DecodeResponse(string challenge, string str)
        {
            if (str.Length > 100) return 0;
            var shuzi = new[] { 1, 2, 5, 10, 50 };
            var chongfu = "";
            var key = new Hashtable();
            var count = 0;
            for (var i = 0; i < challenge.Length; i++)
            {
                var item = challenge.ElementAt(i) + "";
                if (chongfu.Contains(item)) continue;
                var value = shuzi[count % 5];
                chongfu += item;
                count++;
                key.Add(item, value);
            }
            var res = str.Sum(t => (int)key[t + ""]);
            res = res - DecodeRandBase(challenge);
            return res;
        }

        /// <summary>
        /// 引用代码
        /// js下载官方sdk https://github.com/GeeTeam/gt-csharp-sdk
        /// </summary>
        private void Ps()
        {
            /* public async System.Threading.Tasks.Task<ActionResult> GetCaptcha()
             {
                 var geetest = new GeetestLib(GeetestConfig.PublicKey, GeetestConfig.PrivateKey);
                 var userId = "test";
                 var gtServerStatus = await geetest.PreProcess(userId);
                 Session[GeetestLib.GtServerStatusSessionKey] = gtServerStatus;
                 Session["userID"] = userId;
                 HttpContext.Response.ContentType = "application/json";
                 return Content(geetest.GetResponseStr());

             }
            [HttpPost]
             public async System.Threading.Tasks.Task<ActionResult> Index1(string geetest_challenge,string geetest_validate,string geetest_seccode)
             {
                 var geetest = new GeetestLib(GeetestConfig.PublicKey, GeetestConfig.PrivateKey);
                 var gtServerStatusCode = (byte)Session[GeetestLib.GtServerStatusSessionKey];
                 var userId = (string)Session["userID"];
                 int result;
                //var challenge = Request.Form.Get(GeetestLib.FnGeetestChallenge);
                //var validate = Request.Form.Get(GeetestLib.FnGeetestValidate);
                //var seccode = Request.Form.Get(GeetestLib.FnGeetestSeccode);
                var challenge = geetest_challenge;
                var validate = geetest_validate;
                var seccode = geetest_seccode;
                if (gtServerStatusCode == 1)
                    result = await geetest.EnhencedValidateRequest(challenge, validate, seccode, userId);
                else
                    result = geetest.FailbackValidateRequest(challenge, validate, seccode);
                switch (result)
                {
                    case 1:
                        return Content("success");
                    default:
                        return Content("fail");
                }
            }
             
             */
        }
    }
}
